![]() Acrobat file (147K) |
![]() ClarisWorks 4 file (50K) |
![]() not available yet |
Technote 1065 |
September 1996 |
By Lawrence D'Oliveiro
ldo@waikato.ac.nz
The University of Waikato, New Zealand
QuickDraw GX was designed entirely with the C programmer in mind. As a consequence, the user of a high-level language, such as Pascal or Modula-2, is left unable to take advantage of all the capabilities of GX. This Technote outlines how you can get around this hurdle.
This Technote is aimed at programmers with some 68K assembly-language experience, who have an aversion to the C programming language. You will need to be familiar with the Macintosh Programmer's Workshop (MPW) environment, if only because that hosts the only 68K assembler on the Macintosh worth using. I will discuss how to make use of all the functionality of GX from a language like Pascal or Modula-2, without writing any C code.
The discussion breaks the problem down into all the important cases, and gives an example of how to deal with each. Based on this, you can construct your own adaptation of Apple's GX interfaces, or you can make use of the adaptation in Modula-2 that I have already done. The examples use Modula-2 rather than Pascal, mainly because Modula-2 allows the definition of routine types, which Pascal does not.
The GX Printing Manager uses an odd mixture of C and Pascal calling conventions. All application-level printing calls (and most calls back to application routines, apart from message overrides) use Pascal conventions, whereas calls made from drivers and extensions (and calls made to them from GX) follow C conventions.
Ancillary services, used heavily by the GX printing architecture, are provided by the Message Manager and the Collection Manager. The Collection Manager uses Pascal conventions exclusively, so I don't need to discuss it further. The Message Manager uses C conventions exclusively.
There are two halves to the problem: your code calling GX, and GX calling your code. The second is by far the more difficult one.
You can further simplify this in the situation where the routine takes no more than one argument: in this case, there is no need to reorder the argument list--you can pass the compiler-generated list directly to the GX routine. In fact, the glue becomes so simple, it can be done as an inline.
Following are examples of all of the cases that can be handled with inlines. These can be adapted simply by changing the selector code as appropriate. First, the simplest possible case: a GX routine that takes no arguments and returns no result:
PROCEDURE GXEnterGraphics; CODE 0705FH, (* moveq.l #$5F, d0 *) 0A832H; (* _Skia *)It so happens that all the calls with no arguments and no result have selector codes between 0 and 127, hence the above example can be adapted directly, including the use of the moveq instruction.
Here is one with a result but no arguments:
PROCEDURE GXNewInk() : gxInk; CODE 0303CH, 0009DH, (* move.w #$9D, d0 *) 0A832H, (* _Skia *) 02E80H; (* move.l d0, (sp) *)This one has an argument but no result:
PROCEDURE GXDrawShape ( source : gxShape ); CODE 0303CH, 000DCH, (* move.w #$DC, d0 *) 0A832H, (* _Skia *) 0588FH; (* addq.l #4, sp *)And this one has both a single argument and a result:
PROCEDURE GXNewShape ( aType : gxShapeType ) : gxShape; CODE 0303CH, 0009EH, (* move.w #$9E, d0 *) 0A832H, (* _Skia *) 0588FH, (* addq.l #4, sp *) 02E80H; (* move.l d0, (sp) *)When you have more than one argument, then it becomes easier to call an assembly-language glue routine, than to use an inline. The repetitiveness of the structure of the glue makes it natural--in fact, imperative--to use assembly-language macros to generate it.
lea 4(sp), a0 ; address of argument list move.l (a0)+, -(sp) ; copy arguments in reverse order move.l (a0)+, -(sp) ; repeat as many times as necessary ... move.w #TrapCode, d0 dc.w $A832 ; call the GX routine add.l #NrArgs*4, sp ; pop the copied argument list move.l (sp)+, a0 ; get the return address add.l #NrArgs*4, sp ; pop the original argument list move.l d0, (sp) ; return the result (if appropriate) jmp (a0) ; back to callerMy actual macros make a few embellishments to this. First off, not all the arguments are actually longwords: there are a couple of layout routines (GXGetOffsetGlyphs and GXGetGlyphOffset) with arguments that are more naturally represented as booleans. The glue will take care of converting all arguments to longwords, as GX expects.
Here is a basic macro to generate the GX glue:
_Skia opword $A832 macro GXCall &ModuleName, &RoutineName, &ArgList, &Result=, &TrapCode .* General macro for generating call glue for both A-trap routines .* and library routines. .* ModuleName is the name of the Modula-2 definition module .* containing the external declaration; RoutineName is the name of .* the routine. ArgList is the list of arguments, .* each argument being of the formAnd here is an example of how to use the above. First, the original C prototype of the GX routine:. (name is ignored, .* but is useful for checking purposes), Result specifies the .* function result size (b, w, l for byte word or long, omit if .* no result), and TrapCode is the A-trap selector. seg '&ModuleName' &ModuleName._&RoutineName proc export lcla &ArgIndex, &ArgListLength, &NrArgs, &ResultSize lclc &ThisArgSize, &ThisArg &NrArgs seta &nbr(&ArgList) if &NrArgs <> 0 then lea 4(sp), a0 endif if &Result <> '' then if &Result = 'b' then &ResultSize seta 1 elseif &Result = 'w' then &ResultSize seta 2 elseif &Result = 'l' then &ResultSize seta 4 else aerror &Concat('Unrecognized result size: ', &Result) endif else .* no result &ResultSize seta 0 endif &ArgListLength seta 0 &ArgIndex seta &NrArgs while &ArgIndex > 0 do &ThisArg setc &ArgList[&ArgIndex] &ThisArgSize seta &pos('.', &ThisArg) &ThisArg setc &ThisArg[&ThisArgSize + 1 : 99] if &ThisArg = 'b' then &ThisArgSize seta 2 move.b (a0)+, d0 tst.b (a0)+ ; bytes are pushed as words extb.l d0 move.l d0, -(sp) elseif &ThisArg = 'w' then &ThisArgSize seta 2 move.w (a0)+, d0 ext.l d0 move.l d0, -(sp) elseif &ThisArg = 'l' then &ThisArgSize seta 4 move.l (a0)+, -(sp) else aerror &Concat('Unrecognized arg size: ', &ArgList[&ArgIndex]) endif &ArgListLength seta &ArgListLength + &ThisArgSize &ArgIndex seta &ArgIndex - 1 endwhile move.w #&TrapCode, d0 _Skia if &NrArgs <> 0 then add.l #&NrArgs*4, sp endif move.l (sp)+, a0 if &ArgListLength <> 0 then add.l #&ArgListLength, sp endif if &ResultSize = 1 then move.b d0, (sp) elseif &ResultSize = 2 then move.w d0, (sp) elseif &ResultSize = 4 then move.l d0, (sp) endif jmp (a0) endproc endm
void GXGetOffsetGlyphs ( gxShape layout, gxByteOffset trial, long leadingEdge, gxLayoutOffsetState *offsetState, unsigned short *firstGlyph, unsigned short *secondGlyph );Here's how I declared it in Modula-2:
PROCEDURE GXGetOffsetGlyphs ( layout : gxShape; trial : gxByteOffset; leadingEdge : BOOLEAN; VAR offsetState : gxLayoutOffsetState; VAR firstGlyph : ShortCard; VAR secondGlyph : ShortCard );Here's the macro call to generate the glue:
GXCall GXLayoutRoutines, GXGetOffsetGlyphs, (layout.l, trial.l, leadingEdge.b, offsetState.l, firstGlyph.l, secondGlyph.l), , $15And a quick dump of the generated object code:
LEA $0004(A7),A0 MOVE.L (A0)+,-(A7) MOVE.L (A0)+,-(A7) MOVE.L (A0)+,-(A7) MOVE.B (A0)+,D0 TST.B (A0)+ EXTB.L D0 MOVE.L D0,-(A7) MOVE.L (A0)+,-(A7) MOVE.L (A0)+,-(A7) MOVE.W #$0015,D0 DC.W $A832 ; TB 0032 ADDA.W #$0018,A7 MOVEA.L (A7)+,A0 ADDA.W #$0016,A7 JMP (A0)Note the use of the EXTB instruction for extending the byte boolean to a long. That's only valid on 68020 and better processors--but then, GX won't run on anything less!
This part is best broken into two main cases, with different applicable methods of attack. The first is, when calling a GX routine, how to pass the address of one of your routines for it to call back. The second is how to write a GX printing extension or driver in a language that uses Pascal calling conventions.
The case I will use as an example is how to define the gxSpoolProcedure callback that you pass to GX when flattening and unflattening shapes. One nice thing about defining your own interfaces is that you can make tweaks to improve their usability in some way. For instance, here is how I define the type of a gxSpoolProcedure in Modula-2:
gxSpoolProcedure = PROCEDURE ( (*command :*) gxSpoolCommand, (*block :*) gxSpoolBlockPtr, (*arg :*) ADDRESS (* meaning is up to caller *) ) : LONGINT;The difference from the "official" definition is the addition of an extra argument, which allows me to pass additional information to the routine. This information is passed to the glue-generating routine, and bound into the generated wrapper code, so GX doesn't need to know it is being passed!
Here is the structure for holding the wrapper glue code:
TYPE BoundgxSpoolProcedure = ARRAY [1 .. 13] OF ShortCard;And here is the routine that takes the address of your spool callback routine and the value of the extra argument, and actually generates the wrapper glue:
PROCEDURE BindgxSpoolProcedure ( TheProc : gxSpoolProcedure; ToArg : ADDRESS; VAR Result : BoundgxSpoolProcedure ); (* creates a closure of the specified spool procedure which can be passed where QuickDraw GX expects one. *) BEGIN Result[1] := 041EFH; (* lea n(sp), a0 *) Result[2] := 00004H; (* n for above *) Result[3] := 042A7H; (* clr.l -(sp) *) (* room for result *) Result[4] := 02F18H; (* move.l (a0)+, -(sp) *) (* copy command arg *) Result[5] := 02F18H; (* move.l (a0)+, -(sp) *) (* copy block arg *) Result[6] := 02F3CH; (* move.l #n, -(sp) *) (* insert additional arg *) Result[7] := HiWrd(ToArg); (* n for above *) Result[8] := LoWrd(ToArg); (* ditto *) Result[9] := 04EB9H; (* jsr l *) Result[10] := HiWrd(CAST(LongCard, TheProc)); (* l for above *) Result[11] := LoWrd(CAST(LongCard, TheProc)); (* ditto *) Result[12] := 0201FH; (* move.l (sp)+, d0 *) (* return result where C expects it *) Result[13] := 04E75H; (* rts *) FlushCaches END BindgxSpoolProcedure;As an example of how to use the above, here is my version of the HandleToShape routine from the Apple-provided GX library source code. Note in particular how the extra argument is used to pass the environment pointer to allow access to outer local variables from the inner routine:
PROCEDURE HandleToShape ( TheHandle : Handle; NrViewPorts : LONGINT; ViewPorts : gxViewPortPtr (* array *) ) : gxShape; (* unflattens a handle into a new shape. Any graphics errors are automatically posted. *) VAR SrcOffset : LONGCARD; BoundSpoolFromHandle : BoundgxSpoolProcedure; HandleSpool : gxSpoolBlock; PROCEDURE SpoolFromHandle ( command : gxSpoolCommand; VAR block : gxSpoolBlock ) : LONGINT; (* spool routine to retrieve the flattened shape data from TheHandle. *) BEGIN SAVEREGS; CASE VAL(INTEGER, command) OF | GXGraphicsTypes.gxReadSpool: BlockMoveData ( (*sourcePtr :=*) TheHandle^ + SrcOffset, (*destPtr :=*) block.buffer, (*byteCount :=*) block.count ); SrcOffset := SrcOffset + VAL(LONGCARD, block.count) ELSE (* ignore *) END (*CASE*); RETURN 0 END SpoolFromHandle; BEGIN (*HandleToShape*) SrcOffset := 0; BindgxSpoolProcedure ( (*TheProc :=*) CAST(GXUseful.gxSpoolProcedure, ADR(SpoolFromHandle)), (*ToArg :=*) CurrentA6(), (*@Result :=*) BoundSpoolFromHandle ); HandleSpool.spoolProcedure := ADR(BoundSpoolFromHandle); HandleSpool.buffer := NIL; (* let GX allocate the buffer *) HandleSpool.bufferSize := 0; (* ditto *) RETURN GXUnflattenShape ( (*@block :=*) HandleSpool, (*count :=*) NrViewPorts, (*portList :=*) ViewPorts ) END HandleToShape;
Thus, I hit upon the idea of writing an MPW tool, called PrintingSegment, to automatically generate the glue code. The result takes the form of a .o object file that you link at the front of your "pext" or "pdvr" code segment. The same tool can also generate the "over" resources that tell GX printing where the routine entry points are and which messages they handle. Thus, the tool also solves the problem of keeping the "over" resources in sync.
I did make one simplification: I decided that giving the tool the information necessary to copy and convert the argument list was too much work. Instead, I pass a single pointer to the argument list as the argument to the Pascal-conformant routine.
For example, consider the interface for an override for the gxDespoolPage message. In Apple's C interfaces, this is defined as:
typedef OSErr (*GXDespoolPageProcPtr) ( gxSpoolFile theSpoolFile, long numPages, gxFormat theFormat, gxShape *thePage, Boolean *formatChanged );The following record structure exactly mirrors the layout of the C argument list:
TYPE BooleanPtr = POINTER TO BOOLEAN; gxShapePtr = POINTER TO gxShape; GXDespoolPageArgs = RECORD theSpoolFile : gxSpoolFile; PageNumber : LONGINT; theFormat : gxFormat; thePage : gxShapePtr; formatChanged : BooleanPtr END (*RECORD*);(Note that all the C arguments are passed as longwords.) So the Pascal-conformant override for this message is declared as:
PROCEDURE HandleDespoolPage ( VAR Args : GXDespoolPageArgs ) : OSErr;and the glue that PrintingSegment generates to call this override looks like this:
CLR.W -(A7) PEA $0006(A7) JSR HandleDespoolPage MOVE.W (A7)+,D0 RTSHere is what the body of HandleDespoolPage might look like, in a printing extension that allows the user to reverse the order in which pages are printed:
PROCEDURE HandleDespoolPage ( VAR Args : GXDespoolPageArgs ) : OSErr; (* override for gxDespoolPage message. *) VAR ThisPageNumber : LONGINT; Err : OSErr; BEGIN SAVEREGS; IF ReverseOrderPrinting THEN ThisPageNumber := NrPages - Args.PageNumber + 1 ELSE ThisPageNumber := Args.PageNumber END (*IF*); Err := ForwardGXDespoolPage ( (*theSpoolFile :=*) Args.theSpoolFile, (*PageNumber :=*) ThisPageNumber, (*theFormat :=*) Args.theFormat, (*@thePage :=*) Args.thePage^, (*@formatChanged :=*) Args.formatChanged^ ); RETURN Err END HandleDespoolPage;